gitlet

Author: QingZhiLiangCheng

Contributors: QingZhiLiangCheng, ChengShi

Since: 2025-05-10

Github: https://github.com/QingZhiLiangCheng/gitlet

Bilibili: https://www.bilibili.com/video/BV1WfjmzKEm4/?spm_id_from=333.1007.top_right_bar_window_history.content.click


Overview

这是针对伯克利大学CS61B, spring2021 Project2构建的Gitlet.

Gitlet是一个版本管理系统,仿照主流系统Git的功能并实现了其部分基本命令,包括init,add,commit,rm,checkout,branch,reset,rm-branch,merge 等. 官方并没有提供实质性的框架, 而是要自己设计具体使用哪些类,使用哪些数据结构.

Features

GetStart

在运行Gitlet之前, 确保在根目录下运行下面的shell命令. 将会在根目录下建立一个classes文件夹 然后将所有.java

文件通过javac编译成.class文件并存入classes文件夹

我是在Ubantu Linux的运行环境下执行的命令

#cd 根目录 gitletmkdir gitlet-test  
javac -d gitlet-test gitlet/*.java  
cd gitlet-test  
ls  

为了开始使用Gitlet,需要先通过命令初始化一个新的Gitlet仓库.

java gitlet.Main init # 初始化gitlet仓库  

The implemented command

java gitlet.Main init  
  
java gitlet.Main add [file name]  
  
java gitlet.Main commit [commit message]  
  
java gitlet.Main rm [file name]  
  
java gitlet.Main status  
  
java gitlet.Main log  
  
java gitlet.Main checkout [commit id] -- [file name]  
  
java gitlet.Main branch [branch name]  
  
java gitlet.Main rm-branch [branch name]  
  
java gitlet.Main checkout [branch name]  
  
java gitlet.Main checkout -- [file name]  
  
java gitlet.Main checkout [commit id] -- [file name]  
  
java gitlet.Main merge [branch name]  

Gitlet Design Document

structure

在开始之前,首要要弄明白git中的一些重要的对象

sha1

Git和Gitlet都以SHA-1的加密哈希函数,该函数可从任意字节序列生成160位整数哈希值

加密哈希函数具有这样的特性:很难找到两个具有相同哈希值的不同字节流。所以我们也使用id作为了文件名

commit文件

每一次提交都会存储为commit文件,commit文件中包含了必要的提交时间,提交信息,父节点id,以及版本中的各个文件所对应的blob。

public class Commit implements Serializable {  
    private String id;    
    private String message;    
    private Timestamp timestamp;    
    private List<String> parents;    
    private HashMap<String, String> blobMap;
}  

blobs

blob是存取文件内容的文件. 当修改文件的内容后, 会创建新的blob文件, 每个commit文件都会存储其版本每个文件所对应的内容blob的blob id.

public class Blob implements Serializable {  
    private String content;    
    private String id;    
    private File filePath;
}  

Branch

分支事实上是一个pointer, 指向的是这个分支最后的commit

所以我们创建了Pointer类

public class Pointer implements Serializable {  
    public String next;  
    public Pointer(String id) {    
        next = id;    
    }
}  

Branch类继承了Pointer类,同时Branch类有自己的branchName

public class Branch extends Pointer {  
    private final String branchName;
}  

HEAD

HEAD指向的是你当前所在的分支的最新提交, 同时包含了该分支的名字

public class Head extends Pointer {  
    private final String branchName;
}  

Repository

Repository类是管理仓库各种操作的类,在这个类中我们明确了gitlet的目录结构, 并且实现了不同的命令操作

最终,我们实现的.gitlet目录如下所示

gitlet (folder)
    |── objects (folder)
        |-- commits (folder)
        |-- blobs (folder)
    |── refs (folder)
        |── heads (folder)
            |-- master (file)
            |-- other file      
        |-- HEAD (folder)
             |-- HEAD (file)
    |-- addstage (folder)   
    |-- removestage (folder) 

具体实现上, 我们通过Commit Manager, Head Manager, Branch Manager, Blob Manager, Add Stage Manager, Remove Stage Manager 六个manager分别管理不同实体类在不同精确目录中的操作

public class Repository {
  /**
   * current working directory.
   */
  private static File CWD = new File(System.getProperty("user.dir"));
  
  /**
   * the .gitlet directory.
   */
  public static File GITLET_DIR = join(CWD, ".gitlet");
  

  /**
   * the objects directory<br>
   * 包含commits 和 blogs
   */
  public static File OBJECTS_DIR =join(GITLET_DIR, "objects");


  /**
   * the refs directory.<br>
   * 包含heads 和 HEAD<br>
   * - heads 存分支
   */
  public static File REFS_DIR = join(GITLET_DIR, "refs");


  /**
   * Done(QingZhiLiangCheng): 重构
   * 增加add manager, remove manager, blob manager, commit manager, head manager,branch manager来管理对于仓库的操作
   * commit, head, branch, blob 等类仅仅作为实体
   */
  private final CommitManager commitManager;
  private final BlobManager blobManager;
  private final AddStageManager addStageManager;
  private final HeadManager headManager;
  private final BranchManager branchManager;
  public static RemoveStageManager removeStageManager;
}

Main

在Main.java中, 其实很简短,就是通过命令行args判断参数的类型以调用不同的命令

Init

init命令java gitlet.Main init的作用是初始化仓库

所谓的初始化,需要完成以下任务

gitlet (folder)
    |── objects (folder)
        |-- commits (folder)
            |-- <hash> <-- init commit
        |-- blobs (folder)
    |── refs (folder)
        |── heads (folder)
            |-- master (file) <-- 默认生成master分支
            |-- other file      
        |-- HEAD (folder)
             |-- HEAD (file) <-- 保存HEAD指针的对应commit id
    |-- addstage (folder)   
    |-- removestage (folder)

add

java gitlet.Main add [file name]

将文件加入add暂存区

.gitlet (folder)
    |── objects (folder) 
        |-- commits (folder)
        |-- blobs (folder)
            |-- <hash>  <----- 加入的file.txt文件内容
    |── refs (folder)
        |── heads (folder) 
            |-- master (file)
            |-- other file     
        |-- HEAD (folder)     
            |-- HEAD (file)
    |-- addstage (folder)       
        |-- file.txt  <----- 保存blob文件的id
    |-- removestage (folder)

file.txt  <----- 加入的文件

commit

java gitlet.Main commit [commit message]

.gitlet (folder)
    |── objects (folder) 
        |-- commits (folder)
            | -- <hash> <----- 添加进的commit文件
        |-- blobs (folder)
            |-- <hash>  
    |── refs (folder)
        |── heads (folder) 
            |-- master (file)
            |-- other file     
        |-- HEAD (folder)     
            |-- HEAD (file)   
    |-- addstage (folder)   <----- 将add stage的文件加入commit
    |-- removestage (folder) <---- 将remove stage从commit移除

rm

java gitlet.Main rm [file name]

.gitlet (folder)
    |── objects (folder) 
        |-- commits (folder) 
            | -- <hash> 
        |-- blobs (folder) 
            |-- <hash>  
    |── refs (folder)
        |── heads (folder) 
            |-- master (file)
            |-- other file     
       |-- HEAD (folder)     
            |-- HEAD (file)   
    |-- addstage (folder)       <----- 若是在addstage中有则删除
    |-- removestage (folder)
        |-- file.txt  <----- 添加
file.txt  <----- 若是在被track状态,则进行删除;若不是在track,就不能删除

status

java gitlet.Main status

=== Branches ===  
*master  
other-branch  
 === Staged Files ===  
wug.txt  
wug2.txt  
  
=== Removed Files ===  
goodbye.txt  
 === Modifications Not Staged For Commit ===  
junk.txt (deleted)  
wug3.txt (modified)  
  === Untracked Files ===  
random.stuff  
   

log

java gitlet.Main log

这里值得注意的是, 官方文档说"Starting at the current head commit, display information about each commit backwards along the commit tree until the initial commit, following the first parent commit links, ignoring any second parents found in merge commits. " 所以说对于普通的提交,顺着父提交打印就行

对于合并提交:需要显示该提交的信息,包括两个父提交的简短哈希值,但仅沿着第一个父提交继续向上回溯历史。

我们最终打印出来的时间戳跟官网文档中的并不一样,因为我们使用的是java.sql的包

===  
commit 310c73b74c44429beba493c4acdc520bf0448e7b  
Date: 2025-05-18 21:59:23.279  
Message: commit Hello.txt and aaa.txt  
Files: Hello.txt aaa.txt  
  
===  
commit cf1284a045bdb52ab00bd47da90e5f16e048ce38  
Date: 1970-01-01 08:00:00.0  
Message: initial commit  
  

branch

java gitlet.Main branch [branch name]

创建一个指定名称的新分支,并让它指向当前的HEAD提交

但是值得注意的是,这个命令不会立即切换到新创建的分支(就像真实的 Git 一样),直到java gitlet.Main checkout branchName

才会切换了分支

.gitlet (folder)  
    |── objects (folder)        |-- commits (folder)   
            | -- <hash>   
        |-- blobs (folder)   
            |-- <hash>    
    |── refs (folder)  
        |── heads (folder)            |-- master (file)  
            |-- other file     <----- 指向当前头部提交  
        |-- HEAD (folder)            |-- HEAD (file)          
    |-- addstage (folder)         
    |-- removestage (folder)  
file.txt  

rm-branch

java gitlet.Main rm-branch [branch name]

删除指定名称的分支, 但这仅仅意味着删除与该分支相关联的指针;并不会删除在该分支下创建的所有提交等内容

.gitlet (folder)  
    |── objects (folder)        |-- commits (folder)   
            | -- <hash>   
        |-- blobs (folder)   
            |-- <hash>    
    |── refs (folder)  
        |── heads (folder)            |-- master (file)  
            |-- other file     <----- 将此文件删除  
        |-- HEAD (folder)            |-- HEAD (file)          
    |-- addstage (folder)         
    |-- removestage (folder)  
file.txt  

checkout

java gitlet.Main checkout [commit id] -- [file name]

java gitlet.Main checkout [commit id] -- [file name]

从指定的提交中检出(恢复)某个文件到工作目录 不修改暂存区

java gitlet.Main checkout [branch name]

java gitlet.Main checkout [branch name]

java gitlet.Main checkout -- [file name]

java gitlet.Main checkout -- [file name]

Merge

java gitlet.Main merge [branch name]

下表展示了常见情况:

文件 分割点 *master main 是否冲突 说明
a.txt ✅ 有 ❌ 无变化 ❌ 无变化 文件无变更
b.txt ✅ 有 ✅ 修改 ❌ 无变化 当前分支修改,目标未动
c.txt ✅ 有 ❌ 无变化 ✅ 修改 目标分支修改,当前未动
d.txt ✅ 有 ✅ 修改 ✅ 相同修改 都修改但相同,自动合并
e.txt ✅ 有 ✅ 修改 ✅ 不同修改 ✅ 是 内容不同,产生冲突
f.txt ✅ 有 ✅ 删除 ❌ 无变化 当前删除,目标未动,文件被删
g.txt ✅ 有 ❌ 无变化 ✅ 删除 目标删除,当前未动,文件被删
h.txt ✅ 有 ✅ 删除 ✅ 删除 都删除,没冲突
1.txt ❌ 无 ✅ 添加 ❌ 无变化 当前新增文件
2.txt ❌ 无 ❌ 无变化 ✅ 添加 目标新增文件
3.txt ❌ 无 ✅ 添加 ✅ 添加(不同) ✅ 是 都添加但内容不同,冲突
4.txt ❌ 无 ✅ 添加 ✅ 添加(相同) 都添加且内容相同,自动合并